\chapter{Signal I/O系统的设计}

\section{总体思路}
在Tenon语言里，用户可以定义中断变量ivar和中断处理函数，可以用connect语句将中断变量连接到
中断处理函数上。这个时候，中断变量ivar还没有关联到任何具体的中断上。

系统的具体实现（也就是控制器）需要提供扩展函数，给用户提供将中断变量关联到具体中断上的功能。例如
isignaldi(ivar, signalvar) 将一个中断变量关联到一个输入信号所对应的中断上。为了让语言执行器能够触发
ivar这个中断变量，需要使用\verb|tenon_add_irq_handler|接口函数为ivar建立一个中断处理器IrqHandler对
象。中断处理器描述一个ivar对应的中断的相关性质，例如是否是使能的，是否是一次性的等。

当建立好中断变量ivar到具体中断源的关联，以及建立了中断变量的中断处理描述对象之后，就可以通过
\verb|tenon_ack_irq|接口函数来在中断发生的时候，触发中断变量ivar, 进而触发ivar所连接的中断处理函数的
调用了。

Tenon支持9类中断，每一类中断可以注册若干个中断处理器IrqHandler，每个中断处理器对应着一个中断变量。

系统的具体实现需要为每一类中断注册一个描述对象，对应的接口是\verb|Executor::registerIrqHandlerClass|.
这个描述对象提供了系统用于操作中断处理器IrqHandler对象的方法。例如，在添加IrqHandler对象时，系统需要
比较两个IrqHandler对象是否相同，用于避免重复添加IrqHandler。

除了以中断方式处理I/O信号之外，系统还需要提供一些扩展函数用于程序直接处理信号I/O.

\section{具体机制}
\subsection{用户用例}
从Tenon语言用户的角度看，信号中断的处理由以下几个步骤组成。

\begin{itemize}
    \item 定义信号变量(signalxx类型)，中断变量(intnum类型)，中断处理函数。
    \item 使用connect将中断变量连接到中断处理函数上。connect由语言提供。
    \item 使用aliasio将信号变量和实际信号关联起来。aliasio由控制器提供实现。
    \item 使用isignalxx将信号变量和中断变量关联起来。isignalxx由控制器提供实现。
\end{itemize}

另外，用户还可以使用setdo, waitdi等函数，不通过中断，在程序中进行直接的信号通信处理。

\subsection{系统机制}
信号IO的设计分为语言层面和控制器层面两个部分。

在语言层面，Tenon提供的机制包括：

\begin{itemize}
    \item 6种signalxx opaque类型，用于定义信号类型的模块变量。
    \item \verb|Executor::registerIrqHandlerClass|接口函数，用于控制器向语言环境注册中断处理器类。
    \item \verb|tenon_add_irq_handler|接口函数，用于控制器向语言环境添加中断处理器的实例。
    \item \verb|tenon_ack_irq|接口函数，用于控制器向语言环境触发信号中断。
    \item \verb|tenon_get_global_var_name_by_address|接口函数，用于获取signalxx/intnum变量的名称
\end{itemize}

在控制器层面，厂商需要实现以下几个方面的功能。

\begin{itemize}
    \item 在初始化语言环境时，通过\verb|tenon_register_irq_handler_class|接口函数注册6种signalxx的中断处理器类。
    \item 实现6种isignalxx函数，建立信号变量与中断变量之间的联系。
          通过\verb|tenon_add_irq_handler|接口函数向语言执行环境添加中断处理器的实例。
    \item aliasio扩展函数，用于建立信号变量与真实的信号之间的联系。
    \item setdo, waitdi等扩展函数，用于程序进行主动信号处理。
    \item 在信号中断发生时，调用\verb|tenon_ack_irq|接口函数，用于向语言环境触发信号中断。
\end{itemize}

在以上的设计中，只有语言层面的机制是固定的，控制器层面的扩展函数可以根据需求自行定义，只需要能够在
中断变量、信号变量和真实信号之间建立固定的联系即可。

\begin{figure}[H]
    \includegraphics[scale=0.6]{ioarch}
    \caption{IO中断处理架构}
\end{figure}

\section{Executor::registerIrqHandlerClass}

Executor::registerIrqHandlerClass方法用于向Tenon系统注册中断处理器类，通常需要在系统初始化的时候调用。
Tenon系统一共包含9类中断，缺省提供了定时器中断、持久化数据值变更中断的中断处理器类。厂商可以注册其它的中断处理器类，
也可以重写Tenon系统提供的缺省中断处理器类。其函数原型如下：

\begin{verbatim}
    void Executor::registerIrqHandlerClass(IrqKind kind,
                        irq_handler_match_t cmpf, irq_handler_release_t relf,
                        irq_handler_save_t savef, irq_handler_recover_t recf);
\end{verbatim}

各参数的含义如下：

\begin{itemize}
    \item kind 要添加的中断处理器类的类型。
    \item cmpf 中断处理器实例的比较方法。用于判断中断处理器的实例是否重复添加的。
    \item relf 中断处理器实例的释放方法。用于删除中断处理器时，释放中断处理器的资源。
    \item savef 中断处理器实例的序列化方法。用于保存/恢复时，将中断处理器的实例序列化到磁盘。
    \item recf 中断处理器的反序列化方法。用于保存/恢复时，从磁盘恢复出中断处理器的实例。
\end{itemize}

序列化/反序列化中断处理器的实例时，Tenon执行器会分别调用中断处理器实例的savef/recf指向的函数来完成此工作。
有两种工作方式：

\begin{itemize}
    \item savef指向的函数返回一个字符串。语言环境会保存该字符串。在恢复执行环境时，会调用注册的recf函数，将
        该字符串作为参数。savef函数要实现中断处理器实例到字符串的转换，recf函数要实现从字符串恢复中断配置。
    \item savef指向的函数返回一个空指针，则语言环境只保存中断实例的基本信息。调用recf函数时，字符串参数为空串。
        控制器需要根据实例中的ivar等信息恢复中断配置。
\end{itemize}

在保存/恢复中断处理器的实例时，可以调用tenon\_get\_global\_var\_name\_by\_address方法来获取中断变量
（通过下面IrqHandler中的ivar字段）的名称。从而可以知道当前的处理器实例是关联在哪个中断变量上的。

\section{tenon\_add\_irq\_handler}

\verb|tenon_add_irq_handler|接口为一个中断变量ivar创建一个中断处理器IrqHandler对象。通常在实现
isignalxx函数时，需要调用本接口来向语言环境注册一个中断处理器。Tenon用户程序通过isignalxx函数来注册
一个从信号变量到中断变量的联系，当信号发生时，即会触发该中断。

中断处理器IrqHandler描述了语言系统应如何处理一个由ivar标识的中断。当通过\verb|tenon_ack_irq|向语言系
统发送由ivar标识的中断时，语言系统会根据对应的IrqHandler对象中保存的信息决定是否触发这个中断。若触发
中断，系统会调用ivar所关联的中断处理函数。当保存和恢复中断处理器时，也会传递IrqHandler对象给相应的函
数。

其函数原型如下：

\begin{verbatim}
IrqHandler* tenon_add_irq_handler(TenonState state, IrqKind kind, SingleMode mode,
                                  InterruptRef ivar, void* data);
\end{verbatim}

返回值：返回的为中断处理器实例的指针，指针类型为IrqHandler*，其定义如下：

\begin{verbatim}
    typedef struct irq_handler_struct IrqHandler;
    struct irq_handler_struct {
        IrqKind kind;
        SingleMode mode;
        InterruptRef ivar;
        bool disabled;
        bool triggered;
        void* data;
        void* ectx;
    };
\end{verbatim}

各字段的含义如下：

\begin{itemize}
    \item kind 该中断处理器所能处理的中断事件的类型。
    \item mode 中断处理器实例的触发模式。
    \item ivar 中断变量，作为中断处理器的唯一标识，关联到中断处理函数上。
    \item disabled 该中断处理器实例是否是禁止的。
    \item triggered 该中断处理器是否已被触发过。
    \item data 中断处理器实例使用到的私有数据，用户可以通过这个字段让handler携带相关的数据给
          保存和恢复函数。
    \item ectx 实际上是Tenon的执行时上下文环境。
\end{itemize}

tenon\_add\_irq\_handler方法各参数的含义如下：

\begin{itemize}
    \item state 实际上是Tenon的执行时上下文环境
    \item kind 参考IrqHandler的相应字段的描述。
    \item mode 参考IrqHandler的相应字段的描述。
    \item ivar 参考IrqHandler的相应字段的描述。
    \item data 参考IrqHandler的相应字段的描述。
\end{itemize}

每个中断变量ivar只能注册一个中断处理实例，即，中断变量是中断处理实例的标识。

可以参考系统builtin的itimer函数的实现。

\section{tenon\_ack\_irq}

\verb|tenon_ack_irq|用于信号中断发生时，向语言环境发送中断。

当信号发生时，控制器系统检测之前通过isignalxx建立的信号中断中是否有对应的信号，若有且符合触发条件，
则通过\verb|tenon_ack_irq|向语言环境触发中断。

其函数原型如下：

\begin{verbatim}
    void tenon_ack_irq(TenonState state, InterruptRef ivar);
\end{verbatim}

各参数的含义如下：

\begin{itemize}
    \item state 实际上是语言执行时上下文环境
    \item ivar 中断处理器实例绑定的中断变量。
\end{itemize}

可以参考Tenon中builtin中itimer的实现。

\section{tenon\_get\_global\_var\_name\_by\_address}

\verb|tenon_get_global_var_name_by_address|用于根据全局变量的指针（地址）获取该全局变量的变量名。
对于signalxx类型或者intnum类型的变量，作为参数传递给扩展函数的实现时，传递的其实是该全局变量的指针。
可以用\verb|tenon_get_global_var_name_by_address|来获取该全局变量的变量名。
一般来讲，SignalRef或InterruptRef类型的变量都可以通过此接口获得变量名。

在保存和恢复中断处理实例时，需要使用变量名来建立中断变量和信号变量的对应，因为指针地址是会变化的。

函数原型如下：

\begin{verbatim}
    const char* tenon_get_global_var_name_by_address(TenonState state, void* addr);
\end{verbatim}

各参数的含义如下：

\begin{itemize}
    \item state 实际上是语言执行时上下文环境
    \item addr 需要获取变量名的全局变量的指针。
\end{itemize}

\section{示例：中断的订阅和isignalxx函数组}

控制器厂商可实现6种isignalxx函数：

\begin{itemize}
    \item isignaldi
    \item isignaldo
    \item isignalgi
    \item isignalgo
    \item isignalai
    \item isignalao
\end{itemize}

这6个函数可以分别实现对di、do、gi、go、ai、ao等信号的中断的订阅，即实现用中断变量标识相应的信号中断。
在这些函数的实现中，需要实现：

\begin{itemize}
    \item 按照一定的参数向系统申请中断；
    \item 建立signalxx信号变量和中断变量的联系；
    \item 当系统通知控制器信号中断发生时，控制器需要通过\verb|tenon_ack_irq|接口通知Tenon系统该中断的发生
\end{itemize}

\section{示例：aliasio扩展函数}

\verb|opaque void aliasio(anytype& sigvar, string signame);|

\begin{itemize}
    \item 参数sigvar：signalxx变量，指定Tenon程序中的信号变量
    \item 参数signame: string, 指定控制器中的信号名
\end{itemize}

\verb|aliasio|用于建立从语言中的信号变量到控制器中的IO信号之间的联系。建立了变量到信号的联系之后，控制器就
知道信号变量对应的是哪个具体的IO信号。从而其他的IO函数，如waitdi，就可以被控制器执行了。

在aliasio的实现中，需要用\verb|tenon_get_anytype_ref|接口获得信号变量的\verb|void*|指针作为信号变量的
固定标识。在其他的扩展函数中，也需要向Tenon系统传递这个\verb|void*|指针作为信号变量的标识。

实现示例：

\begin{verbatim}
void aliasio(void* obj, TenonState ctx, TenonData sigvar, TenonData signame) {
    void* varRef = tenon_get_anytype_ref(ctx, sigvar);
    const char* sigName = tenon_get_string_value(ctx, signame);
    bool result = static_cast<SignalManager*>(obj)->aliasIO(varRef, sigName);
    if (!result)
        tenon_throw_error(ctx, "err_aliasio_def");
}
\end{verbatim}

\section{示例：setdo扩展函数}

\verb|opaque void setdo(signaldo& out, num val);|

\begin{itemize}
    \item 参数out: signaldo类型，指定要输出的信号变量
    \item 参数val: 指定要输出的信号值
\end{itemize}

\verb|setdo|函数用于向数字输出信号输出一个值。

实现示例：

\begin{verbatim}
void setdo(void* obj, TenonState ctx, TenonData sig, TenonData val) {
    void* varRef = tenon_get_anytype_ref(ctx, sig);
    int ival = static_cast<int>(tenon_get_num_value(ctx, val));
    static_cast<SignalManager*>(obj)->setdo(varRef, ival);
}
\end{verbatim}

\section{示例：waitdi扩展函数}

\verb|opaque void waitdi(signaldi& di, num val);|

\begin{itemize}
    \item 参数di: 指定要等待的信号变量
    \item 参数val: 指定要等待的信号值
\end{itemize}

\verb|waitdi|函数用于等待一个数字输入信号变为某个值。

实现示例：

\begin{verbatim}
void waitdi(void* obj, TenonState ctx, TenonData sig, TenonData val) {
    void* varRef = tenon_get_anytype_ref(ctx, sig);
    int ival = static_cast<int>(tenon_get_num_value(ctx, val));
    static_cast<SignalManager*>(obj)->waitdi(varRef, ival);
}
\end{verbatim}

\section{一个完整的Tenon程序例子}

\begin{verbatim}
module module1 {

void dihandler() {
    print("input2 triggered");
}

signaldo do1;
signaldi di1;
signaldi di2;
intnum intv;

void main() {
    connect(intv, dihandler);
    isignaldi(di2, 1, intv);        
    
    aliasio(do1, "do1");
    aliasio(di1, "input1");
    aliasio(di2, "input2");
    setdo(do1, 1);
    waitdi(di1, 1);
    print("input1: 1");
}
}
\end{verbatim}
